#!/bin/sh
#
# swiki -- A simple wiki generator written in POSIX sh.
#
# Copyright (C) 2025  Ricardo García Jiménez <ricardogj08@riseup.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#

SWIKI_VERSION=1.0
SWIKI_REPO=https://notabug.org/ricardogj08/swiki

# Muestra mensajes de errores.
errors() {
  printf '%s.\n' "$1" >&2
  exit 1
}

# Estilos CSS de la wiki.
styles() { cat <<EOF
body {
  font-family: sans-serif;
  font-size: 16px;
  padding: 0;
  margin: 0;
  background-color: #fff;
  color: #000;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.navbar {
  clear: both;
  color: #069;
  overflow: hidden;
  background-color: #17a;
  padding: 0.7ex;
  border-top: 1px solid;
  border-top-color: #ccc;
  border-bottom: 1px solid #069;
}

.navbar a {
  padding: 0.5ex 1ex;
  color: #fff;
}

.navbar a:hover {
  background-color: #069;
}

.navbar a, .sidebar a {
  text-decoration: none;
}

.left {
  float: left;
  margin: 0;
  padding: 0;
}

.right {
  float: right;
  margin: 0;
  padding: 0;
}

a {
  color: #069;
}

.sidebar {
  float: left;
  margin: 0 1px 0 0;
  padding: 1em 0;
  border-right: 1px dotted #ccc;
  width: 200px;
}

.sidebar ul {
  margin: 0;
  padding: 0;
}

.sidebar li {
  list-style: none;
  padding: 0;
  margin: 0;
}

.sidebar li ul {
  padding-left: 0.6em;
}

.sidebar li a {
  display: block;
  margin: 0;
  padding: 0.8ex 2em 0.8ex 1em;
}

.sidebar a:hover, th, blockquote, pre, code {
  background-color: #ddd;
}

.content {
  clear: both;
  margin: 0;
  padding: 0;
  flex: 1;
}

.main {
  margin: 0 0 0 200px;
  max-width: 50em;
  padding: 1.5em;
}

.footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.7ex;
  text-align: center;
}

.footer p {
  margin: 0.5ex 1ex;
}

h1, h2, h3, h4, h5, h6 {
  margin: 1em 1ex 0.5ex 0;
}

h2~*:not(h2) {
  margin-left: 1ch;
}

h1 {
  font-size: 2.0em;
}

h2 {
  font-size: 1.9em;
}

h3 {
  font-size: 1.6em;
}

h4 {
  font-size: 1.5em;
}

h5 {
  font-size: 1.2em;
}

h6 {
  font-size: 1.1em;
}

pre, code {
  margin: 0;
  font-size: 100%;
}

pre {
  padding: 1ch;
}

pre, table {
  display: block;
  max-width: 100%;
  overflow-x: auto;
}

table {
  border-collapse: collapse;
}

th, td {
  border: 1px solid #ccc;
  padding: 0.7ex;
}

blockquote::before {
  float: left;
  width: 1ch;
  margin-right: 1ch;
  content: ">";
}

img {
  max-width: 100%;
}

@media (max-width: 999px) {
  .sidebar {
    float: none;
    margin: 0;
    width: 100%;
    border: none;
  }

  .main {
    margin: 0;
  }

  .footer {
    flex-direction: column;
  }
}

@media (prefers-color-scheme: dark) {
  body {
    background-color: #000;
    color: #fff;
  }

  .navbar {
    border-top-color: #222;
  }

  .sidebar a:hover, th {
    background-color: #222;
  }

  a {
    color: #6cf;
  }

  .main img[src$=svg] {
    filter: invert(1);
  }

  blockquote, pre, code {
    background-color: #111;
    border-color: #aaa;
  }
}
EOF
}

# Plantilla base para todas las páginas de la wiki.
template() { cat <<EOF
<!DOCTYPE html>
<html lang="${SWIKI_LANG}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="icon" href="${SWIKI_FAVICON}">
  <title>${PAGE_TITLE}</title>
  <style>${PAGE_STYLES}</style>
</head>
<body>
  <nav class="navbar">${PAGE_NAVBAR}</nav>
  <div class="content">
    <aside class="sidebar">${PAGE_SIDEBAR}</aside>
    <main class="main">${PAGE_BODY}</main>
  </div>
  <div>
    <hr>
    <footer class="footer">
      <p>Generated with <a href="${SWIKI_REPO}" target="_blank">swiki</a>, a simple wiki generator written in POSIX <code>sh</code>.</p>
      <p>${SWIKI_LICENSE}</p>
    </footer>
  </div>
</body>
</html>
EOF
}

# Genera una etiqueta html de enlace.
anchor() {
  printf '<a href="%s">%s</a>\n' "$1" "$2"
}

# Busca archivos markdown en el directorio actual.
find_markdown() {
  find . -type f -name \*.md -exec ls -X {} + ||
    errors 'No se puede encontrar los archivos markdown'
}

# Elimina las diagonales del final de un string.
rtrim_slashes() {
  printf '%s\n' "${1%%"${1##*[!/]}"}"
}

# Elimina "." y "./" del comienzo de un string.
ltrim_dotslash() {
  printf '%s\n' "${1##"${1%%[!./]*}"}"
}

# Cambia la extensión ".md" a ".html" de un string.
extmd2html() {
  printf '%s.html\n' "${1%.md}"
}

# Convierte un archivo markdown a html.
markdown2html() {
  smu "$1" 2>/dev/null
}

# Obtiene el título principal de un string en html.
get_title() {
  title=$(printf '%s\n' "$1" | grep -se '^<h1>' | head -n 1)

  title=${title#<h1>}
  title=${title%</h1>}

  printf '%s\n' "${title}"

  unset -v title
}

# Genera una etiqueta html bold.
bold() {
  printf '<b>%s</b>\n' "$1"
}

# Genera una etiqueta html de apertura de lista desordenada.
open_list() {
  printf '<ul>\n'
}

# Genera una etiqueta html de cierre de lista desordenada.
close_list() {
  printf '</ul>\n'
}

# Genera una etiqueta html de apertura de item de lista.
open_item() {
  printf '<li>\n'
}

# Genera una etiqueta html de cierre de item de lista.
close_item() {
  printf '</li>\n'
}

# Obtiene el directorio base de un string.
get_basedir() {
  basedir=$(ltrim_dotslash "${1%/*}")

  printf '%s\n' "./${basedir%%/*}"

  unset -v basedir
}

# Cuenta el número de archivos de un directorio.
count() {
  [ -e "$1" ] \
      && printf '%s\n' "$#" \
      || printf '%s\n' 0
}

# Genera el sidebar para cada página de la wiki.
sidebar() {
  open_list

  # Itera sobre los archivos del directorio.
  for entry in "$1"/*; do
    # Obtiene el directorio base del item.
    ENTRY_BASEDIR=$(get_basedir "${entry}")

    # Considera solo los directorios raíz y
    # el directorio al que pertenece el archivo.
    ! { [ "${ENTRY_BASEDIR}" = ./ ] || [ "${ENTRY_BASEDIR}" = "${PAGE_BASEDIR}" ]; } && continue

    # Path del item.
    ENTRY_PATH="${entry}"

    # Comprueba que el item exista y sea un archivo regular o directorio.
    if [ -f "${entry}" ]; then
      # Igonara archivos "index.md"
      [ "${entry##*/}" = index.md ] && continue

      # Comprueba si es un archivo con extensión ".md".
      [ "${entry%.md}" = "${entry}" ] && continue
    elif [ -d "${entry}" ]; then
      # Comprueba si el directorio contiene archivo markdown.
      [ "$(count "${entry}"/*.md)" -le 0 ] && continue

      # Establece el path del item si es un directorio.
      [ -f "${entry}/index.md" ] && ENTRY_PATH=${ENTRY_PATH}/index.md
    else
      continue
    fi

    # Obtiene el título de la entrada.
    ENTRY_TITLE=$(get_title "$(markdown2html "${ENTRY_PATH}")")

    # Genera el path del item.
    ENTRY_PATH=$(ltrim_dotslash "${ENTRY_PATH}")

    # Coloca en negritas la ruta base y el archivo actual.
    [ "${entry}" = "${PAGE_BASEDIR}" ] || [ "${entry}" = "$2" ] &&
      ENTRY_TITLE=$(bold "${ENTRY_TITLE}")

    # Genera la url del item.
    ENTRY_URL=${SWIKI_URL}/$(extmd2html "${ENTRY_PATH}")

    open_item

    # Genera el enlace del item.
    anchor "${ENTRY_URL}" "${ENTRY_TITLE}/"

    # Itera de manera recursiva.
    [ -d "${entry}" ] && sidebar "${entry}" "$2"

    close_item
  done

  close_list
}

# Genera una etiqueta html span posicionada a la derecha.
span_rigth() {
  printf '<span class="right">%s</span>\n' "$1"
}

# Obtiene los items del navbar.
get_navbar_items() {
  printf '%s\n' "$1" | markdown2html -- | sed 's/<\/\?p>//g'
}

# Genera el navbar de la wiki.
navbar() {
  anchor "${SWIKI_URL}/index.html" "$(bold "${SWIKI_HOME}")"

  get_navbar_items "${SWIKI_LEFT_NAVBAR}"

  span_rigth "$(get_navbar_items "${SWIKI_RIGHT_NAVBAR}")"
}

# Construye la wiki.
build_wiki() {
  # Genera el navbar de todos los archivos.
  PAGE_NAVBAR=$(navbar)

  # Genera los estilos de todos los archivos.
  PAGE_STYLES=$(styles)

  # Itera sobre todos los archivos markdown de la wiki.
  find_markdown | while read -r file; do
    # Genera el path del archivo markdown a html.
    PAGE_PATH=$(extmd2html "${file}")

    # Genera la url del archivo.
    PAGE_URL=${SWIKI_URL}/$(ltrim_dotslash "${file}")

    # Obtiene el contenido del archivo markdown a html.
    PAGE_BODY=$(markdown2html "${file}")

    # Obtiene el título del archivo.
    PAGE_TITLE=$(get_title "${PAGE_BODY}")

    # Directorio base del archivo.
    PAGE_BASEDIR=$(get_basedir "${file}")

    # Genera el sidebar del archivo.
    PAGE_SIDEBAR=$(
      cd -- "${SWIKI_PWD}" &&
      cd -- "${SWIKI_SOURCE}" &&
      sidebar . "${file}")

    # Renombra el archivo markdown a html.
    mv "${file}" "${PAGE_PATH}"

    # Renderiza la página html.
    template > "${PAGE_PATH}"
  done
}

# Muestra un mensaje de ayuda.
shelp() { cat <<EOF
swiki ${SWIKI_VERSION} - A simple wiki generator written in POSIX sh.

Synopsis:
  swiki [OPTIONS]

Options:
  -f - Configuration file path.
  -i - Source path of markdown files.
  -o - Output path of html files.
  -h - Show this message.
EOF
exit 0
}

# Función principal.
main() {
  while getopts :f:hi:o: option; do
    case $option in
      f) SWIKI_CONFIG=${OPTARG};;
      i) SWIKI_SOURCE=${OPTARG};;
      o) SWIKI_OUTPUT=${OPTARG};;
      \?|h) shelp;;
      :) errors "La opción -${OPTARG} require un argumento"
    esac
  done

  # Decrementa el puntero del argumento "$@"
  # para que apunte al siguiente argumento.
  shift $((OPTIND - 1))

  # shellcheck source=/dev/null
  [ -r "${SWIKI_CONFIG:=./swiki.cfg}" ] && . "${SWIKI_CONFIG}"

  # Establece configuraciones por defecto.
  : "${SWIKI_OUTPUT:=./docs}"
  : "${SWIKI_SOURCE:=./src}"
  : "${SWIKI_URL:=${PWD}/docs}"
  : "${SWIKI_HOME:=Home}"
  : "${SWIKI_LANG:=es}"
  : "${SWIKI_LICENSE:=© CC-BY-4.0}"
  : "${SWIKI_FAVICON:=data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>📘</text></svg>}"

  SWIKI_SOURCE=$(rtrim_slashes "${SWIKI_SOURCE}")
  SWIKI_URL=$(rtrim_slashes "${SWIKI_URL}")
  SWIKI_PWD=${PWD}

  command -v smu >/dev/null ||
    errors 'Requiere que smu se encuentre instalado'

  mkdir -p "${SWIKI_OUTPUT}" "${SWIKI_SOURCE}" ||
    errors 'No se pueden crear los directorios de trabajo'

  cp -R -fp "${SWIKI_SOURCE}/." "${SWIKI_OUTPUT}" ||
    errors 'No se puede acceder a los directorios de trabajo'

  cd -- "${SWIKI_OUTPUT}" ||
    errors 'No se puede acceder al directorio de salida'

  build_wiki
}

main "$@"
